package com.baidu.iit.pxp.util;

import com.baidu.iit.pvm.ActivityException;
import com.baidu.iit.pvm.PvmActivity;
import com.baidu.iit.pvm.PvmProcessDefinition;
import com.baidu.iit.pvm.PvmScope;
import com.baidu.iit.pvm.delegate.ActivityExecution;
import com.baidu.iit.pvm.process.ActivityImpl;
import com.baidu.iit.pvm.process.ProcessDefinitionImpl;
import com.baidu.iit.pvm.process.ScopeImpl;
import com.baidu.iit.pvm.runtime.AtomicOperation;
import com.baidu.iit.pvm.runtime.InterpretableExecution;
import com.baidu.iit.pxp.BpmnError;
import com.baidu.iit.pxp.behavior.EventSubProcessStartEventActivityBehavior;
import com.baidu.iit.pxp.entity.ExecutionEntity;
import com.baidu.iit.pxp.model.ErrorEventDefinitionData;
import com.baidu.iit.pxp.parser.BpmnParse;

import java.util.List;

/**
 * User: huangweili
 * Date: 14-4-28
 * Time: 下午5:43
 */
public class ErrorPropagation {

    public static void propagateError(BpmnError error, ActivityExecution execution) throws Exception {
        propagateError(error.getErrorCode(), execution);
    }

    public static void propagateError(String errorCode, ActivityExecution execution) throws Exception {

        while (execution != null) {
            String eventHandlerId = findLocalErrorEventHandler(execution, errorCode);
            if (eventHandlerId != null) {
                executeCatch(eventHandlerId, execution, errorCode);
                break;
            }
            execution = getSuperExecution(execution);
        }
        ;
        if (execution == null) {
            throw new BpmnError(errorCode, "No catching boundary event found for error with errorCode '"
                    + errorCode + "', neither in same process nor in parent process");
        }
    }


    private static String findLocalErrorEventHandler(ActivityExecution execution, String errorCode) {
        PvmScope scope = execution.getActivity();
        while (scope != null) {

            List<ErrorEventDefinitionData> definitions = (List<ErrorEventDefinitionData>) scope.getProperty(BpmnParse.PROPERTYNAME_ERROR_EVENT_DEFINITIONS);
            if (definitions != null) {
                for (ErrorEventDefinitionData errorEventDefinition : definitions) {
                    if (errorEventDefinition.catches(errorCode)) {
                        return scope.findActivity(errorEventDefinition.getHandlerActivityId()).getId();
                    }
                }
            }
            if (scope instanceof PvmActivity) {
                scope = ((PvmActivity) scope).getParent();
            } else {
                scope = null;
            }
        }
        return null;
    }


    private static ActivityExecution getSuperExecution(ActivityExecution execution) {
        ExecutionEntity executionEntity = (ExecutionEntity) execution;
        ExecutionEntity superExecution = executionEntity.getProcessInstance().getSuperExecution();
        if (superExecution != null && !superExecution.isScope()) {
            return superExecution.getParent();
        }
        return superExecution;
    }

    private static void executeCatch(String errorHandlerId, ActivityExecution execution, String errorCode) {
        ProcessDefinitionImpl processDefinition = ((ExecutionEntity) execution).getProcessDefinition();
        ActivityImpl errorHandler = processDefinition.findActivity(errorHandlerId);
        if (errorHandler == null) {
            throw new ActivityException(errorHandlerId + " not found in process definition");
        }

        boolean matchingParentFound = false;
        ActivityExecution leavingExecution = execution;
        ActivityImpl currentActivity = (ActivityImpl) execution.getActivity();

        ScopeImpl catchingScope = errorHandler.getParent();
        if (catchingScope instanceof ActivityImpl) {
            ActivityImpl catchingScopeActivity = (ActivityImpl) catchingScope;
            if (!catchingScopeActivity.isScope()) { // event subprocesses
                catchingScope = catchingScopeActivity.getParent();
            }
        }

        if (catchingScope instanceof PvmProcessDefinition) {
            executeEventHandler(errorHandler, ((ExecutionEntity) execution).getProcessInstance(), errorCode);

        } else {
            if (currentActivity.getId().equals(catchingScope.getId())) {
                matchingParentFound = true;
            } else {
                currentActivity = (ActivityImpl) currentActivity.getParent();

                while (!matchingParentFound && leavingExecution != null && currentActivity != null) {
                    if (!leavingExecution.isConcurrent() && currentActivity.getId().equals(catchingScope.getId())) {
                        matchingParentFound = true;
                    } else if (leavingExecution.isConcurrent()) {
                        leavingExecution = leavingExecution.getParent();
                    } else {
                        currentActivity = currentActivity.getParentActivity();
                        leavingExecution = leavingExecution.getParent();
                    }
                }

                while (leavingExecution != null
                        && leavingExecution.getParent() != null
                        && leavingExecution.getParent().getActivity() != null
                        && leavingExecution.getParent().getActivity().getId().equals(catchingScope.getId())) {
                    leavingExecution = leavingExecution.getParent();
                }
            }

            if (matchingParentFound && leavingExecution != null) {
                executeEventHandler(errorHandler, leavingExecution, errorCode);
            } else {
                throw new ActivityException("No matching parent execution for activity " + errorHandlerId + " found");
            }
        }

    }

    private static void executeEventHandler(ActivityImpl borderEventActivity, ActivityExecution leavingExecution, String errorCode) {
        if (borderEventActivity.getActivityBehavior() instanceof EventSubProcessStartEventActivityBehavior) {
            InterpretableExecution execution = (InterpretableExecution) leavingExecution;
            execution.setActivity(borderEventActivity.getParentActivity());
            execution.performOperation(AtomicOperation.ACTIVITY_START);
        } else {
            leavingExecution.executeActivity(borderEventActivity);
        }
    }

}
